Skip to content

扩展-关于wav PCM格式的阐述

一、PCM和ADPCM

音频压缩

音频编码的出现很大程度上是为了压缩文件体积,大文件除了占用存储空间,对传输带宽也是一笔不小的消耗,这涉及到文件比特率的问题:音频越大,播放时在单位时间需读取的数据量也就越大,还是假设有一段 44.1k 采样率、16bit 位深的立体声 PCM 数据,播放 1 秒钟就需要 44100 × 2Byte × 2 = 172KB,意味着传输速率也必须达到 172KB/s,换算为码率就是 1376 kbps,对系统 IO 性能和数据可靠性都是不小的挑战。

如果事先对音频进行压缩编码,保存为 MP3 格式,能大幅度地降低音频数据量,同等参数下码率仅有 128kbps,传输起来要轻松的多。众多不同的音频压缩编码,采用的算法原理不尽相同,但有着同样的目的 —— 为了节约数据的存储和传输带宽。MP3 通过去除频域上人耳不敏感的部分减少数据量,包括霍夫曼编码、频谱重排列、IMDCT 变换等复杂过程,相比之下,ADPCM 编码格式就简单得多,解码器也更容易实现。

PCM是差分脉冲代码调制, 使用有损压缩格式时,某些数据在压缩过程中会更改和丢失。 ADPCM 是自适应查分脉冲编码调制,加入了自适应的差值数组,可以实现高达 4:1 的压缩比的同时,减少数据偏差。

ADPCM 格式

如果我们直接存储每一个点的16位的采样值,这样就需要53X16=848个位,大约是106字节。但我们换个思路,我们不存储采样值,而存储采样点两两之间的差值(采样值可能会很大,需要更多的位数来表达,比如16个位,但是两点之间一般来说是比较连续的,差值不会太大,所以这个差值只需要很少的几个位即可表达,比如4个位)。这样,我们只需要知道前一个点的值,又知道它与下一个点的差值,就可以计算得到下一个点了。这个差值就是所谓的“差分”!

这就是DPCM的主要思想精髓!!

“什么?DPCM?我们不是说ADPCM吗?”是的,DPCM是差分脉码调制,多一个A,就是自适应!为什么要自适应?

你有没有考虑到一个问题?如果取两点之差,这个差值超过了4个位可以表达的范围,该如何处理?音频信号虽然具有很高的连续性,但是我们并不能保证两点之间产生突变的可能!前面的差值可能是3、5、8、12,后面突然变成20、40,甚至更大,这时4个位还够用吗?数据量的减少,不能以数据丢失和失真作为代价!!

如果有一种方法,可以把两点之间的差值变换到固定的几个位即可表达的范围内,那就好了!而且这种变换是实时的,并且具有自适应性和预测能力的。这就是ADPCM的基本思想。它定义了一些因子,这种算法如果发现两点之间差值变大之后,就会用差值去和相应的因子作除法,从而减小了差值,让它可以减少到几个位可表达的数值范围内。而选择哪一个因子来除它,这就是ADPCM编码要作的事情了。

ADPCM算法巧妙的利用了音频信号的特点,也就是音频信号上的点与它前面的若干个点是有一定的相关性的,从而可以对下一个点进行预测,从而预先估计这个差值,从而选取相应的除数因子,去把差值归化到数值范围内!!以上就是ADPCM对音频信号进行编码大体思想和过程!

由于声音信号具有波形上的连续性,因此相邻两个采样值大小也非常接近,记录单个采样值通常需要 16bit,而记录前后两个采样点的差值(差分法),往往只需要 4bit,这便是 ADPCM 压缩编码的基本原理,因此通过 ADPCM 编码的音频文件,其大小只有 PCM 格式的四分之一。

不仅如此,ADPCM 的智能之处在于,对于变化剧烈的波形,算法通过自适应机制,能自动改变差分值的度量粒度,即使是抖动较大的信号,也可以保证前后采样差值总能用固定的 4bit 表示。在 PCM 编码的基础上增加 「差分」和「自适应」的特性,便是 ADPCM(Adaptive Differential Pulse Code Modulation 自适应差分脉冲编码调制) 名称的由来。

当然,ADPCM 算法实现简单、压缩率高的同时,必然要付出音质损失的代价 —— ADPCM 格式文件的声音听起来会略为粗糙,被同样是有损压缩的 MP3 编码吊打,不过用于提示音、人声讲话等场合还是绰绰有余。

实现 ADPCM 解码器

学习知识的一个好方法是动手实践,通过实际代码能加深对技术的理解,接下来将会介绍如何编写一个 ADPCM 解码器 —— 将 ADPCM 数据还原为 PCM 格式,但在开始之前,要先选定一个 ADPCM 标准作为依据。是的,ADPCM 下面还分了好几类,有 YAMAHA 、Microsoft、IMA 等标准,不同的处理流程也会有细分的规范。下面选择 Microsoft 格式(MS ADPCM)作为我们实现解码器的标准,并参考 FFmpeg 的代码,开启「照抄模式」。

Microsoft ADPCM 标准

有关 MS ADPCM 解码器的编程和实现,看懂这两个网页就够了:

  1. https://wiki.multimedia.cx/index.php/Microsoft_ADPCM
  2. https://ffmpeg.org/doxygen/3.1/adpcm_8c_source.html 第一个页面是 MultimediaWiki 对 Microsoft ADPCM 的基本介绍、技术细节及实现解码方式的详细描述,理解这些内容有助于我们看代码,做到心中有数,至少不会一脸懵逼。下面解释一下其中比较关键的信息。

block 构成

MS ADPCM 音频数据由许多 block(块)组成,每个块包含前言(preamble)和半字节序列(series of nibbles)两部分,preamble 包括一些自适应差分相关参数以及两个原始采样数据,半字节序列是编码后的 ADPCM 采样值序列。单个 block 的大小在 wav 头的 nBlockAlign 字段中表示。其中单声道和双声道的 preamble 格式又有些区别: 单声道 preamble 格式

byte 0 block predictor (builtin predictors are in the range [0..6] but others can be manually defined) bytes 1-2 initial delta bytes 3-4 sample 1 bytes 5-6 sample 2

立体声 preamble 格式

byte 0 left channel block predictor (should be [0..6]) byte 1 right channel block predictor (should be [0..6]) bytes 2-3 left channel initial idelta bytes 4-5 right channel initial idelta bytes 6-7 left channel sample 1 bytes 8-9 right channel sample 1 bytes 10-11 left channel sample 2 bytes 12-13 right channel sample 2

preamble 中主要包含了实现自适应差分机制相关的参数,除此除外在末尾还存储了两个原始采样值,这不难理解:由于差分过程是通过比较与前一值的差异得到当前值,因此需要两个初始采样(反映此 block 波形变化剧烈程度)作为基础,从而计算相关自适应参数,并解码半字节序列递推解出整一个 block 的 PCM 数据,依次处理所有 block 完成整个音频的解码。

数据解码

借助 preamble 中的参数变量外加三个表格,再经过一系列的运算便能解码 ADPCM 数据,得到完整 16bit PCM 格式音频,计算过程如下:

  • predictor = ((sample1 * coeff1) + (sample2 * coeff2)) / 256
  • predictor += (signed)nibble * delta (note that nibble is 2's complement)
  • clamp predictor within signed 16-bit range
  • PCM sample = predictor
  • send PCM sample to the output
  • shuffle samples: sample 2 = sample 1, sample 1 = calculated PCM sample
  • compute next adaptive scale factor: delta = (AdaptationTable[nibble] * delta) / 256
  • saturate delta to lower bound of 16

AdaptationTable、AdaptCoeff1 和 AdaptCoeff2三个表格存储了计算中用到的一些因子系数,页面中也给出了数组定义:

int AdaptationTable [] = { 
  230, 230, 230, 230, 307, 409, 512, 614, 
  768, 614, 512, 409, 307, 230, 230, 230 
} ;
// These are the 'built in' set of 7 predictor value pairs; additional values can be added to this table by including them as metadata chunks in the WAVE header
int AdaptCoeff1 [] = { 256, 512, 0, 192, 240, 460, 392 } ;
int AdaptCoeff2 [] = { 0, -256, 0, 64, 0, -208, -232 } ;

FFmpeg 代码解析

在网上能找到许多 ADPCM 的示例代码,但有许多人评论不能用、有杂音,这往往是编码标准没对上,或者使用姿势不正确,甚至代码本身就有问题。与其用零散的不规范例程,还不如直接去经典大项目里「抠代码」,规范性统一性和用法都有参考,FFmpeg 项目则是音视频编解码领域的「代码库」。

FFmpeg ADPCM 核心解码模块:FFmpeg: libavcodec/adpcm.c Source File

在文件末尾的宏函数列表中包含了 FFmpeg 所实现的所有 ADPCM 标准,其中的 AV_CODEC_ID_ADPCM_MS 是本文要实现的 MS 格式。

ADPCM_DECODER(AV_CODEC_ID_ADPCM_IMA_RAD,     sample_fmts_s16,  adpcm_ima_rad,     "ADPCM IMA Radical");
ADPCM_DECODER(AV_CODEC_ID_ADPCM_IMA_SMJPEG,  sample_fmts_s16,  adpcm_ima_smjpeg,  "ADPCM IMA Loki SDL MJPEG");
ADPCM_DECODER(AV_CODEC_ID_ADPCM_IMA_WAV,     sample_fmts_s16p, adpcm_ima_wav,     "ADPCM IMA WAV");
ADPCM_DECODER(AV_CODEC_ID_ADPCM_IMA_WS,      sample_fmts_both, adpcm_ima_ws,      "ADPCM IMA Westwood");
ADPCM_DECODER(AV_CODEC_ID_ADPCM_MS,          sample_fmts_s16,  adpcm_ms,          "ADPCM Microsoft");
ADPCM_DECODER(AV_CODEC_ID_ADPCM_MTAF,        sample_fmts_s16p, adpcm_mtaf,        "ADPCM MTAF");
ADPCM_DECODER(AV_CODEC_ID_ADPCM_PSX,         sample_fmts_s16p, adpcm_psx,         "ADPCM Playstation");
adpcm.c 文件包含众多标准的处理,总代码量一千多行,便于分析起见,剔除没用到的代码,保留 MS ADPCM 相关部分,简化后的文件只有两百多行,核心函数是 adpcm_ms_expand_nibble()adpcm_decode_frame():

static inline int16_t adpcm_ms_expand_nibble(ADPCMChannelStatus *c, int nibble)
{
    int predictor;

    predictor = (((c->sample1) * (c->coeff1)) + ((c->sample2) * (c->coeff2))) / 64;
    predictor += ((nibble & 0x08)?(nibble - 0x10):(nibble)) * c->idelta;

    c->sample2 = c->sample1;
    c->sample1 = av_clip_int16(predictor);
    c->idelta = (ff_adpcm_AdaptationTable[(int)nibble] * c->idelta) >> 8;
    if (c->idelta < 16) c->idelta = 16;
    if (c->idelta > INT_MAX/768) {
        av_log(NULL, AV_LOG_WARNING, "idelta overflow\n");
        c->idelta = INT_MAX/768;
    }

    return c->sample1;
}
adpcm_ms_expand_nibble() 函数按照 MultimediaWiki 中描述的解码流程,对 predictor、coeff 和 idelta 进行运算,将 4bit 的 ADPCM 数据 nibble 展开得到 16bit 原始 sample。

函数 adpcm_decode_frame() 则完成对 block 的处理,包括解析出 preamble 和 nibbles 序列数据、计算解码参数初始值、循环调用 adpcm_ms_expand_nibble() 完成解码,代码如下:

static int adpcm_decode_frame(AVCodecContext *avctx, void *data,
                              int *got_frame_ptr, AVPacket *avpkt) 
{
    AVFrame *frame     = data;  
    const uint8_t *buf = avpkt->data;
    int buf_size = avpkt->size;
    ADPCMDecodeContext *c = avctx->priv_data;
    ADPCMChannelStatus *cs;
    int n, m, channel, i;
    int16_t *samples;
    int16_t **samples_p;
    int st; /* stereo */
    int count1, count2;
    int nb_samples, coded_samples, approx_nb_samples, ret;
    GetByteContext gb;

    bytestream2_init(&gb, buf, buf_size);
    nb_samples = get_nb_samples(avctx, &gb, buf_size, &coded_samples, &approx_nb_samples);
    if (nb_samples <= 0) {
        av_log(avctx, AV_LOG_ERROR, "invalid number of samples in packet\n");
        return AVERROR_INVALIDDATA;
    }

    /* get output buffer */
    frame->nb_samples = nb_samples;
    if ((ret = ff_get_buffer(avctx, frame, 0)) < 0)  
        return ret;
    samples = (int16_t *)frame->data[0];
    samples_p = (int16_t **)frame->extended_data;

    /* use coded_samples when applicable */
    /* it is always <= nb_samples, so the output buffer will be large enough */
    if (coded_samples) {
        if (!approx_nb_samples && coded_samples != nb_samples)
            av_log(avctx, AV_LOG_WARNING, "mismatch in coded sample count\n");
        frame->nb_samples = nb_samples = coded_samples;
    }

    st = avctx->channels == 2 ? 1 : 0;

    int block_predictor;

    block_predictor = bytestream2_get_byteu(&gb);
    if (block_predictor > 6) {
        av_log(avctx, AV_LOG_ERROR, "ERROR: block_predictor[0] = %d\n",
                block_predictor);
        return AVERROR_INVALIDDATA;
    }
    c->status[0].coeff1 = ff_adpcm_AdaptCoeff1[block_predictor]; 
    c->status[0].coeff2 = ff_adpcm_AdaptCoeff2[block_predictor]; 
    if (st) {
        block_predictor = bytestream2_get_byteu(&gb);
        if (block_predictor > 6) {
            av_log(avctx, AV_LOG_ERROR, "ERROR: block_predictor[1] = %d\n",
                    block_predictor);
            return AVERROR_INVALIDDATA;
        }
        c->status[1].coeff1 = ff_adpcm_AdaptCoeff1[block_predictor];
        c->status[1].coeff2 = ff_adpcm_AdaptCoeff2[block_predictor];
    }
    c->status[0].idelta = sign_extend(bytestream2_get_le16u(&gb), 16); 
    if (st){
        c->status[1].idelta = sign_extend(bytestream2_get_le16u(&gb), 16);
    }

    c->status[0].sample1 = sign_extend(bytestream2_get_le16u(&gb), 16);
    if (st) c->status[1].sample1 = sign_extend(bytestream2_get_le16u(&gb), 16);
    c->status[0].sample2 = sign_extend(bytestream2_get_le16u(&gb), 16);
    if (st) c->status[1].sample2 = sign_extend(bytestream2_get_le16u(&gb), 16);

    *samples++ = c->status[0].sample2;
    if (st) *samples++ = c->status[1].sample2;
    *samples++ = c->status[0].sample1;
    if (st) *samples++ = c->status[1].sample1;
    for(n = (nb_samples - 2) >> (1 - st); n > 0; n--) {
        int byte = bytestream2_get_byteu(&gb);
        *samples++ = adpcm_ms_expand_nibble(&c->status[0 ], byte >> 4  );
        *samples++ = adpcm_ms_expand_nibble(&c->status[st], byte & 0x0F);
    }

    if (avpkt->size && bytestream2_tell(&gb) == 0) {
        av_log(avctx, AV_LOG_ERROR, "Nothing consumed\n");
        return AVERROR_INVALIDDATA;
    }

    *got_frame_ptr = 1;

    if (avpkt->size < bytestream2_tell(&gb)) {
        av_log(avctx, AV_LOG_ERROR, "Overread of %d < %d\n", avpkt->size, bytestream2_tell(&gb));
        return avpkt->size;
    }

    return bytestream2_tell(&gb);
}

照葫芦画瓢

adpcm_ms_expand_nibble()adpcm_decode_frame() 两个函数的核心功能理解后,抽出主要代码,照葫芦画瓢实现我们自己的 ADPCM 解码器,这是我改的一个版本 lib_adpcm.c,实现了单个 block 解码接口 adpcm_decode_block(),同时简化了一些 FFmpeg 的代码写法,能达到同样的效果:

#include <stdint.h> #include <string.h> #include <limits.h> 
#include "lib_adpcm.h" 
/* These are for MS-ADPCM */
/* ff_adpcm_AdaptationTable[], ff_adpcm_AdaptCoeff1[], and ff_adpcm_AdaptCoeff2[] are from libsndfile */
const int16_t adpcm_AdaptationTable[] = {
    230, 230, 230, 230, 307, 409, 512, 614,
    768, 614, 512, 409, 307, 230, 230, 230
};

/** Divided by 4 to fit in 8-bit integers */
const uint8_t adpcm_AdaptCoeff1[] = {
    64, 128, 0, 48, 60, 115, 98
};

/** Divided by 4 to fit in 8-bit integers */
const int8_t adpcm_AdaptCoeff2[] = {
    0, -64, 0, 16, 0, -52, -58
};

static const int av_clip_int16(int a)
{
    if (a > 32767)
    {
        a = 32767;
    }
    else if (a < -32768)
    {
        a = -32768;
    }

    return a;
}

static int16_t adpcm_ms_expand_nibble(adpcm_block_info *block_info, int nibble)
{
    int predictor;

    predictor = (((block_info->sample1) * (block_info->coeff1)) + ((block_info->sample2) * (block_info->coeff2))) / 64;
    predictor += ((nibble & 0x08)?(nibble - 0x10):(nibble)) * block_info->delta;

    block_info->sample2 = block_info->sample1;
    block_info->sample1 = av_clip_int16(predictor);
    block_info->delta = (adpcm_AdaptationTable[(int)nibble] * block_info->delta) >> 8;
    if (block_info->delta < 16) block_info->delta = 16;
    if (block_info->delta > INT_MAX/768) {
        printf("[adpcm]%s:%d idelta overflow\n", __FUNCTION__, __LINE__);
        block_info->delta = INT_MAX/768;
    }

    return block_info->sample1;
}

int adpcm_decode_block(int16_t *output, uint8_t *input, adpcm_block_info *block_info)
{
    uint8_t *read_ptr = RT_NULL;
    uint32_t nb_samples = 0;
    int n;

    block_info->block_predictor = *input;
    block_info->delta = (int16_t)input[2] << 8 | input[1] ;
    block_info->sample1 = (int16_t)input[4] << 8 | input[3];
    block_info->sample2 = (int16_t)input[6] << 8 | input[5];
    block_info->nibbles_prt = &input[7];

    read_ptr = block_info->nibbles_prt;
    nb_samples = (ADPCM_BLOCK_SIZE-6)*2;
    if (block_info->block_predictor > 6) {
        printf("[adpcm]%s:%d ERROR: block_info->block_predictor = %d\n", __FUNCTION__, __LINE__,
                block_info->block_predictor);
        return -1;
    }

    block_info->coeff1 = adpcm_AdaptCoeff1[block_info->block_predictor];
    block_info->coeff2 = adpcm_AdaptCoeff2[block_info->block_predictor];

    *output++ = block_info->sample2;
    *output++ = block_info->sample1;

    for(n = (nb_samples - 2) / 2; n > 0; n--) 
    {
        int byte = *read_ptr;
        *output++ = adpcm_ms_expand_nibble(block_info, byte >> 4  );
        *output++ = adpcm_ms_expand_nibble(block_info, byte & 0x0F);
        read_ptr++;
    }

    return nb_samples*sizeof(int16_t);
}

在使用上也很方便,ADPCM 也是通过 WAV 容器封装,这部分的解析代码可以复用,只要定位到音频数据位置,接着不停从文件中读取、每次处理 block_size 字节,循环至文件结束即可,测试代码如下:

#define ADPCM_BLOCK_SIZE 1024 
#define PLAY_ADPCM_READ_SIZE ADPCM_BLOCK_SIZE 
#define PLAY_BUFFER_SIZE PLAY_ADPCM_READ_SIZE*4 
int main()
{
    ...
    // 打开文件、解析 WAV 容器
    while (1)
    {
        memset(read_buffer, 0, PLAY_ADPCM_READ_SIZE);
        memset(write_buffer, 0, PLAY_BUFFER_SIZE);
        read_size = fread(read_buffer, 1, PLAY_ADPCM_READ_SIZE, fp);
        if (read_size == 0)
            break;
        decode_byte = adpcm_decode_block((int16_t*)write_buffer, (uint8_t*)read_buffer, &b_info);
        sound_device_write(dev, 0, write_buffer, decode_byte); // 解码后的 PCM 数据写入声卡设备播放
    }
    // 关闭文件、释放资源
    ...
}

ADPCM的编码和解码实现

ADPCM压缩算法

ADPCM(Adaptive Differential Pulse Code Modulation),是一种针对16bits(或8bits或者更高)声音波形数据的一种有损压缩算法,它将声音流中每次采样的16bit数据以4bit存储,所以压缩比1:4.而且压缩/解压缩算法非常简单,所以是一种低空间消耗,高质量高效率声音获得的好途径。保存声音的数据文件后缀名为.AUD的大多用ADPCM压缩。   ADPCM主要是针对连续的波形数据的,保存的是波形的变化情况,以达到描述整个波形的目的,由于它的编码和解码的过程却很简洁,列在后面,相信大家能够看懂。   8bits采样的声音人耳是可以勉强接受的,而16bit采样的声音可以算是高音质了。ADPCM算法却可以将每次采样得到的16bit数据压缩到4bit。需要注意的是,如果要压缩/解压缩得是立体声信号,采样时,声音信号是放在一起的,需要将两个声道分别处理。

ADPCM压缩过程

首先我们认为声音信号都是从零开始的,那么需要初始化两个变量

int index=0,prev_sample=0;

下面的循环将依次处理声音数据流,注意其中的getnextsample()应该得到一个16bit的采样数据,而outputdata()可以将计算出来的数据保存起来,程序中用到的step_table[],index_adjust[]附在后面:

    int index=0,prev_sample:=0;

    while (还有数据要处理)
    {
      cur_sample=getnextsample();        //得到当前的采样数据
      delta=cur_sample-prev_sample;       //计算出和上一个的增量
      if (delta<0) delta=-delta,sb=8;      //取绝对值
      else sb = 0 ;               // sb保存的是符号位
      code = 4*delta / step_table[index];    //根据steptable[]得到一个0-7的值
      if (code>7) code=7;            //它描述了声音强度的变化量
      index += index_adjust[code] ;       //根据声音强度调整下次取steptable的序号
      if (index<0) index=0;           //便于下次得到更精确的变化量的描述
      else if (index>88) index=88;
      prev_sample=cur_sample;
      outputode(code|sb);            //加上符号位保存起来
    }

ADPCM解压缩过程

接压缩实际是压缩的一个逆过程,同样其中的getnextcode()应该得到一个编码,,而outputsample()可以将解码出来的声音信号保存起来。这段代码同样使用了同一个的setp_table[]和index_adjust()附在后面:

    int index=0,cur_sample=0;

    while (还有数据要处理)
    {
        code=getnextcode();                       //得到下一个数据
        if ((code & 8) != 0) sb=1 else sb=0;
        code&=7;                            //将code分离为数据和符号
        delta = (step_table[index]*code)/4+step_table[index]/8;     //后面加的一项是为了减少误差
        if (sb==1) delta=-delta;
        cur_sample+=delta;                       //计算出当前的波形数据
        if (cur_sample>32767) output_sample(32767);
        else if (cur_sample<-32768) output_sample(-32768);
        else output_sample(cur_sample);
        index+=index_adjust[code];
        if (index<0) index=0;
        if (index>88) index=88;
     }
附表
     int index_adjust[8] = {-1,-1,-1,-1,2,4,6,8};

     int step_table[89] =
     {
       7,8,9,10,11,12,13,14,16,17,19,21,23,25,28,31,34,37,41,45,
       50,55,60,66,73,80,88,97,107,118,130,143,157,173,190,209,230,253,279,307,337,371,
       408,449,494,544,598,658,724,796,876,963,1060,1166,1282,1411,1552,1707,1878,2066,
       2272,2499,2749,3024,3327,3660,4026,4428,4871,5358,5894,6484,7132,7845,8630,9493,
       10442,11487,12635,13899,15289,16818,18500,20350,22385,24623,27086,29794,32767
     }
算法介绍: http://www.mp3-tech.org/programmer/docs/adpcm.pdf